﻿using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using UML_Creator.Command;
using UML_Creator.Model;
using System.Xml.Serialization;
using System.IO;

namespace UML_Creator.ViewModel
{
    /// <summary>
    /// Denne ViewModel er bundet til MainWindow.
    /// </summary>
    /// 
    public class MainViewModel : ViewModelBase
    {
        
        public static Dictionary<Type, int> DiagramTypes = new Dictionary<Type, int>();
        public static Dictionary<Type, int> EdgeTypes = new Dictionary<Type, int>();
        // Holder styr på undo/redo.
        private UndoRedoController undoRedoController = UndoRedoController.GetInstance();

        // Bruges til at ændre tilstand når en kant er ved at blive tilføjet.
        private bool isAddingEdge = false;
        private bool isMovingNode = false;
        // Bruges til at gemme det første punkt når en kant tilføjes. 
        private Diagram addingEdgeEndA;
        // Gemmer det første punkt som punktet har under en flytning.
        private Point movingPoint;
        // Gemmer offset fra mus to punktet der flyttes
        private int offsetX;
        private int offsetY;
        // Bruges til at gøre punkterne gennemsigtige når en ny kant tilføjes.
        public double ModeOpacity { get { return isAddingEdge ? 0.4 : 1.0; } }

        private bool moveState = false;
        private int edgeType;

        private List<Diagram> selectedDiagrams = new List<Diagram>();
        private List<Edge> selectedEdges = new List<Edge>();

        //Zoom level
        private double zoom_level = 1.0;
        public double Zoom { get { return zoom_level; } set { zoom_level = value; RaisePropertyChanged("Zoom"); } } 

        private int translateX = 0;
        private int translateY = 0;
        public int TranslateX { get { return translateX; } set { translateX = value; RaisePropertyChanged("TranslateX"); } }
        public int TranslateY { get { return translateY; } set { translateY = value; RaisePropertyChanged("TranslateY"); } }

        //Used to detect double clicks
        private DateTime lastClick = DateTime.Now;

        // Formålet med at benytte en ObservableCollection er at den implementere INotifyCollectionChanged, der er forskellige fra INotifyPropertyChanged.
        // INotifyCollectionChanged smider en event når mængden af elementer i en kollektion ændres (altså når et element fjernes eller tilføjes).
        // Denne event giver GUI'en besked om ændringen.
        // Dette er en generisk kollektion. Det betyder at den kan defineres til at indeholde alle slags klasser, 
        // men den holder kun klasser af en type når den benyttes.
        public ObservableCollection<Diagram> Nodes { get; set; }
        public ObservableCollection<Edge> Edges { get; set; }

        // Kommandoer som UI bindes til.
        public ICommand UndoCommand { get; private set; }
        public ICommand RedoCommand { get; private set; }

        // Kommandoer som UI bindes til.
        public ICommand AddNodeCommand { get; private set; }
        public ICommand AddEdgeCommand { get; private set; }
        public ICommand AddDiamondEdgeCommand { get; private set; }

        // Kommandoer som UI bindes til.

        public ICommand MouseDownNodeCommand { get; private set; }
        public ICommand MouseMoveNodeCommand { get; private set; }
        public ICommand MouseUpNodeCommand { get; private set; }
        public ICommand MouseClickEdgeCommand { get; private set; }
        public ICommand MouseDoubleClickNodeCommand { get; private set; }

        //Commands to move the canvas
        public ICommand MouseDownCanvasCommand { get; private set; }
        public ICommand MouseMoveCanvasCommand { get; private set; }
        public ICommand MouseUpCanvasCommand { get; private set; }

        public ICommand SelectStateCommand { get; private set; }
        public ICommand MoveStateCommand { get; private set; }

        public ICommand DeleteSelectedCommand { get; private set; }

        // Nyt dokument funktion
        public ICommand NewDocument { get; private set; }

        // Gem og load funktioner
        public ICommand SaveFileCommand { get; private set; }
        public ICommand LoadFileCommand { get; private set; }

        // Copy, Cut og Paste funktioner
        public ICommand CopyToClipboardCommand { get; private set; }
        public ICommand CutToClipboardCommand { get; private set; }
        public ICommand PasteFromClipboardCommand { get; private set; }

      //  [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public MainViewModel()
        {
            initTypes();
            // Her fyldes listen af noder med to noder. Her benyttes et alternativ til konstruktorer med syntaksen 'new Type(){ Attribut = Værdi }'
            // Det der sker er at der først laves et nyt object og så sættes objektets attributer til de givne værdier.
            Nodes = new ObservableCollection<Diagram>();
            //FIll some nodes in for testing purposes    
            for (int i = 0; i < 10; i++)
            {
                Nodes.Add(new ClassDiagram("Test0", "description0") { X = (150 * i) + 100 + (i * 3), Y = 50 + i * 51, ClassName = "This is class "+i, ClassAttributes = "Attributes : int\nAttributes : bool", ClassMethods = "Method : int\nMethods : bool" });
            }
                //new ActivityDiagram("Test1", "description") { X = 30, Y = 40, Width = 80, Height = 80 }, 
                //new ActivityDiagram("Test2", "description") { X = 140, Y = 230, Width = 100, Height = 100 } };
            // ElementAt() er en LINQ udvidelses metode som ligesom mange andre kan benyttes på stort set alle slags kollektioner i .NET.
            Edges = new ObservableCollection<Edge>();
            // Kommandoerne som UI kan kaldes bindes til de metoder der skal kaldes. Her vidersendes metode kaldne til UndoRedoControlleren.
            UndoCommand = new RelayCommand(undoRedoController.Undo, undoRedoController.CanUndo);
            RedoCommand = new RelayCommand(undoRedoController.Redo, undoRedoController.CanRedo);

            //Set zoom til default
            Zoom = 1.0;
            // Kommandoerne som UI kan kaldes bindes til de metoder der skal kaldes.
            AddNodeCommand = new RelayCommand<int>(AddNode);
            AddEdgeCommand = new RelayCommand<int>(AddEdge);

            // Kommandoerne som UI kan kaldes bindes til de metoder der skal kaldes.
            MouseDownNodeCommand = new RelayCommand<MouseButtonEventArgs>(MouseDownNode);
            MouseMoveNodeCommand = new RelayCommand<MouseEventArgs>(MouseMoveNode);
            MouseUpNodeCommand = new RelayCommand<MouseButtonEventArgs>(MouseUpNode);
            MouseDoubleClickNodeCommand = new RelayCommand<MouseButtonEventArgs>(MouseDoubleClickNode);

            MouseDownCanvasCommand = new RelayCommand<MouseButtonEventArgs>(MouseDownCanvas);
            MouseMoveCanvasCommand = new RelayCommand<MouseEventArgs>(MouseMoveCanvas);
            MouseUpCanvasCommand = new RelayCommand<MouseButtonEventArgs>(MouseUpCanvas);

            SelectStateCommand = new RelayCommand(SelectState);
            MoveStateCommand = new RelayCommand(MoveState);

            // Nyt dokument
            NewDocument = new RelayCommand(ClearDocument);

            // Save og load
            SaveFileCommand = new RelayCommand(SaveFile);
            LoadFileCommand = new RelayCommand(LoadFile);

            // Copy, Cut og Paste
            CopyToClipboardCommand = new RelayCommand(CopyToClipboard);
            CutToClipboardCommand = new RelayCommand(CutToClipboard);
            PasteFromClipboardCommand = new RelayCommand(PasteFromClipboard);

            MouseClickEdgeCommand = new RelayCommand<MouseButtonEventArgs>(MouseClickEdge);
            
            DeleteSelectedCommand = new RelayCommand(RemoveSelected, CanRemoveSelected);
        }

        private bool CanRemoveSelected()
        {
            return (CanRemoveEdges() || CanRemoveNodes());
        }

        private void RemoveSelected()
        {
            if (CanRemoveNodes()) RemoveNode();
            if (CanRemoveEdges()) RemoveEdges();

            clearSelections();
        }

        private void MoveState()
        {
            clearSelections();
            this.moveState = true;
            Mouse.OverrideCursor = Cursors.Hand;
            
        }
        private void SelectState()
        {
            clearSelections();
        }

        // Tilføjer punkt med kommando.
        public void AddNode(int DiagramType)
        {
            clearSelections();
            Console.WriteLine("Adding Node. Type = {0}", (int)DiagramType);
            undoRedoController.AddAndExecute(new AddNodeCommand(Nodes, DiagramType));
        }

        // Tjekker om valgte punkt/er kan fjernes. Det kan de hvis der er nogle der er valgt.
        public bool CanRemoveNodes()
        {
            return selectedDiagrams.Count > 0;
        }

        // Fjerner valgte punkter med kommando.
        public void RemoveNode()
        {
            undoRedoController.AddAndExecute(new RemoveNodesCommand(Nodes, Edges, selectedDiagrams, selectedEdges));
        }

        // Starter proceduren der tilføjer en kant.
        public void AddEdge(int EdgeType)
        {
            clearSelections();
            isAddingEdge = true;
            edgeType = EdgeType;
            RaisePropertyChanged("ModeOpacity");
        }

        // Tjekker om valgte kant/er kan fjernes. Det kan de hvis der er nogle der er valgt.
        public bool CanRemoveEdges()
        {
            return selectedEdges.Count > 0;
        }

        // Fjerner valgte kanter med kommando.
        public void RemoveEdges()
        {
            undoRedoController.AddAndExecute(new RemoveEdgesCommand(Edges, selectedEdges));
        }

        // Hvis der ikke er ved at blive tilføjet en kant så fanges musen når en musetast trykkes ned. Dette bruges til at flytte punkter.
        public void MouseDownNode(MouseButtonEventArgs e)
        {
            Debug.WriteLine("Node clicked");
            DateTime now = DateTime.Now;
            TimeSpan ts = now - lastClick;
            lastClick = now;
            if (!isAddingEdge && moveState) e.MouseDevice.Target.CaptureMouse();
                else if (!moveState && !isAddingEdge)
                {
                    FrameworkElement nodeElem = (FrameworkElement)e.MouseDevice.Target;
                    Diagram node = (Diagram)nodeElem.DataContext;
            //    Debug.WriteLine("EX: {0}, CX: {1}", node.EX, node.CanvasCenterX);
                    if (selectedDiagrams.Contains(node))
                    {
                        selectedDiagrams.Remove(node);
                        setNodeSelected(node, false);
                    }
                    else
                    {
                        selectedDiagrams.Add(node);
                        setNodeSelected(node, true);
                    }
            
                // Check for doubleclicks
                if (ts.TotalMilliseconds < 250)
            {
                MouseDoubleClickNode(e);
                lastClick = now;
            }
        }
        }

        // Bruges til at flytter punkter.
        public void MouseMoveNode(MouseEventArgs e)
        {
            // Tjek at musen er fanget og at der ikke er ved at blive tilføjet en kant.
            if (Mouse.Captured != null && !isAddingEdge && moveState)
            {
                // Musen er nu fanget af ellipserne og den ellipse som musen befinder sig over skaffes her.
                FrameworkElement movingEllipse = (FrameworkElement)e.MouseDevice.Target;
                // Fra ellipsen skaffes punktet som den er bundet til.
                Diagram movingNode = (Diagram)movingEllipse.DataContext;
                // Canvaset findes her udfra ellipsen.
                Canvas canvas = FindParentOfType<Canvas>(movingEllipse);
                // Musens position i forhold til canvas skaffes her.
                Point mousePosition = Mouse.GetPosition(canvas);
                // Når man flytter noget med musen vil denne metode blive kaldt mange gange for hvert lille ryk, 
                // derfor gemmes her positionen før det første ryk så den sammen med den sidste position kan benyttes til at flytte punktet med en kommando.
                if (movingPoint == default(Point))
                {
                    movingPoint = mousePosition;
                    offsetX = movingNode.CanvasCenterX - (int)mousePosition.X;
                    offsetY = movingNode.CanvasCenterY- (int)mousePosition.Y;
                    isMovingNode = true;
                }
                // Punktets position ændres og beskeden bliver så sendt til UI med INotifyPropertyChanged mønsteret.
                movingNode.CanvasCenterX = (int)mousePosition.X+offsetX;
                movingNode.CanvasCenterY = (int)mousePosition.Y+offsetY;
            }
        }
        // Benyttes til at flytte punkter og tilføje kanter.
        public void MouseUpNode(MouseButtonEventArgs e)
        {
            // Benyttes til at tilføje kanter.
            if (isAddingEdge)
            {
                // Skaffer ellipsen som musen er over.
                FrameworkElement ellipseEnd = (FrameworkElement)e.MouseDevice.Target;
                // Skaffer ellipsens node.
                Diagram nodeEnd = (Diagram)ellipseEnd.DataContext;
                // Hvis det er den første ellipse der er blevet trykket på under tilføjelsen af kanten, så gemmes punktet bare og punktet bliver markeret som valgt.
                if (addingEdgeEndA == null) { addingEdgeEndA = nodeEnd; addingEdgeEndA.IsSelected = true; }
                // Ellers hvis det ikke er den første og de to noder der hører til ellipserne er forskellige, så oprettes kanten med kommando.
                else if (addingEdgeEndA != nodeEnd)
                {
                    undoRedoController.AddAndExecute(new AddEdgeCommand(Edges, addingEdgeEndA, (Diagram)ellipseEnd.DataContext, edgeType));
                    // De tilhørende værdier nulstilles.
                    isAddingEdge = false;
                    RaisePropertyChanged("ModeOpacity");
                    addingEdgeEndA.IsSelected = false;
                    addingEdgeEndA = null;
                }
            }
            // Benyttes til at flytte punkter.
            else if (moveState && isMovingNode)
            {
                // Ellipsen skaffes.
                FrameworkElement movingEllipse = (FrameworkElement)e.MouseDevice.Target;
                // Ellipsens node skaffes.
                Diagram movingNode = (Diagram)movingEllipse.DataContext;
                // Canvaset skaffes.
                Canvas canvas = FindParentOfType<Canvas>(movingEllipse);
                // Musens position på canvas skaffes.
                Point mousePosition = Mouse.GetPosition(canvas);
                // Punktet flyttes med kommando. Den flyttes egentlig bare det sidste stykke i en række af mange men da de originale punkt gemmes er der ikke noget problem med undo/redo.
                undoRedoController.AddAndExecute(new MoveNodeCommand(movingNode, (int)mousePosition.X + offsetX, (int)mousePosition.Y + offsetY, (int)movingPoint.X - offsetX, (int)movingPoint.Y - offsetY));
                // Nulstil værdier.
                movingPoint = new Point();
                isMovingNode = false;
                // Musen frigøres.
                e.MouseDevice.Target.ReleaseMouseCapture();
            }
        }
        public void MouseDownCanvas(MouseButtonEventArgs e)
        {
            //If only we could bubble this event :|
            FrameworkElement movingEllipse = (FrameworkElement)e.MouseDevice.Target;
            if (movingEllipse != null)
              if(movingEllipse.DataContext is Diagram || movingEllipse.DataContext is Edge) return;
            if (!isAddingEdge && moveState) e.MouseDevice.Target.CaptureMouse();
            Debug.WriteLine("Canvas click");
        }

        // Bruges til at flytter punkter.
        public void MouseMoveCanvas(MouseEventArgs e)
        {
            // Tjek at musen er fanget og at der ikke er ved at blive tilføjet en kant.
            if (Mouse.Captured != null && !isAddingEdge && moveState)
            {
 
                // Musen er nu fanget af ellipserne og den ellipse som musen befinder sig over skaffes her.
                FrameworkElement movingEllipse = (FrameworkElement)e.MouseDevice.Target;
                if (movingEllipse.DataContext is Diagram || movingEllipse.DataContext is Edge) return;

                Canvas canvas = FindParentOfType<Canvas>(movingEllipse);
                Point mousePosition = Mouse.GetPosition(canvas);
                if (movingPoint == default(Point))
                {
                    movingPoint = mousePosition;
                    offsetX = (int)mousePosition.X - TranslateX;
                    offsetY = (int)mousePosition.Y - TranslateY;
                    Debug.WriteLine("Is first move: {0},{1}", mousePosition.X, mousePosition.Y);
                }
                // Punktets position ændres og beskeden bliver så sendt til UI med INotifyPropertyChanged mønsteret.
                this.TranslateX = (int)mousePosition.X - offsetX;
                this.TranslateY = (int)mousePosition.Y - offsetY;
                //movingNode.CanvasCenterY = (int)mousePosition.Y - offsetY;
            }
        }
        // Benyttes til at flytte punkter og tilføje kanter.
        public void MouseUpCanvas(MouseButtonEventArgs e)
        {
            FrameworkElement movingEllipse = (FrameworkElement)e.MouseDevice.Target;
            if (movingEllipse.DataContext != null & (movingEllipse.DataContext is Diagram || movingEllipse.DataContext is Edge)) return;
            if (moveState)
            {
                e.MouseDevice.Target.ReleaseMouseCapture();
                movingPoint = default(Point);
            }
        }

        public void MouseClickEdge(MouseButtonEventArgs e)
        {
            if (!moveState)
            {
                FrameworkElement edgeElem = (FrameworkElement)e.MouseDevice.Target;
                Edge selectedEdge = (Edge)edgeElem.DataContext;
                if (selectedEdges.Contains(selectedEdge))
                {
                    selectedEdges.Remove(selectedEdge);
                    setEdgeSelected(selectedEdge, false);
                }
                else
                {
                    selectedEdges.Add(selectedEdge);
                    setEdgeSelected(selectedEdge, true);
                }
            }
        }

        public void MouseDoubleClickNode(MouseButtonEventArgs e)
        {
            FrameworkElement ellipseEnd = (FrameworkElement)e.MouseDevice.Target;
            // Skaffer ellipsens node.
            Diagram nodeEnd = (Diagram)ellipseEnd.DataContext;
            int type;
            nodeEnd.GetType();
            DiagramTypes.TryGetValue(nodeEnd.GetType(), out type);
            undoRedoController.AddAndExecute(new EditNodeCommand(nodeEnd, type));
        }

        // Rekursiv metode der benyttes til at finde et af et grafisk elements forfædre ved hjælp af typen, der ledes højere og højere op indtil en af typen findes.
        // Syntaksen "() ? () : ()" betyder hvis den første del bliver sand så skal værdien være den anden del, ellers skal den være den tredje del.
        private static T FindParentOfType<T>(DependencyObject o) where T : class
        {
            DependencyObject parent = VisualTreeHelper.GetParent(o);
            if (parent == null) return null;
            return parent is T ? parent as T : FindParentOfType<T>(parent);
        }

        private void setNodeSelected(Diagram node, bool selected)
        {
            node.Opacity = (selected) ? 0.4 : 1.0;
            node.Colour = (selected) ? "Blue" : "Black";
        }

        private void setEdgeSelected(Edge edge, bool selected)
        {
            edge.Opacity = (selected) ? 0.4 : 1.0;
            edge.Colour = (selected) ? "Blue" : "Black";
        }

        private void clearSelections()
        {
            foreach (Diagram d in selectedDiagrams) setNodeSelected(d, false);
            foreach (Edge e in selectedEdges) setEdgeSelected(e, false);
            selectedDiagrams.Clear();
            selectedEdges.Clear();
            this.isAddingEdge = false;
            this.moveState = false;
            Mouse.OverrideCursor = null;
        }

        public void SaveFile()
        {
            Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
            dlg.DefaultExt = ".xml";
            dlg.Filter = "XML documents (.xml)|*.xml";
            if (dlg.ShowDialog() == true)
            {
                string filename = dlg.FileName;
                Storage.AsyncSave(filename, Edges, Nodes);
            }
    }

        public void LoadFile()
        {
            Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
            dlg.DefaultExt = ".xml";
            dlg.Filter = "XML documents (.xml)|*.xml";
            if (dlg.ShowDialog() == true)
            {
                string filename = dlg.FileName;
                XmlWrapper wrapper = Storage.AsyncLoad(filename);

                if (wrapper == null)
                {
                    return;
                }
                TranslateX = 0;//Reset view
                TranslateY = 0;
                Nodes.Clear();
                foreach (Diagram diagram in wrapper.Diagrams)
                {
                    Nodes.Add(diagram);
                }
                
                Edges.Clear();
                foreach (Edge edge in wrapper.Edges)
                {
                    foreach (Diagram diagram in Nodes)
                    {
                        if (diagram.ID == edge.EndA.ID)
                        {
                            edge.EndA = diagram;
                        }
                        if (diagram.ID == edge.EndB.ID)
                        {
                            edge.EndB = diagram;
                        }
                    };
                    Edges.Add(edge);
                }

                foreach (Diagram diagram in Nodes)
                {
                    diagram.ID = diagram.GetHashCode();
                }
            }
        }

        public void CopyToClipboard() {
            Console.WriteLine("Copy called");

            ClipboardWrapper data = new ClipboardWrapper(selectedDiagrams, selectedEdges);
            XmlSerializer serializer = new XmlSerializer(typeof(ClipboardWrapper));

            StringWriter writer = new StringWriter();

            serializer.Serialize(writer, data);



            Console.WriteLine(data.Diagrams.Count);
            Console.WriteLine(data.Edges.Count);
            //DataObject clipboardData = new DataObject(typeof(ClipboardWrapper), data);
            Clipboard.SetText(writer.ToString());
        }

        public void CutToClipboard()
        {
            if (CanRemoveSelected())
            {
                CopyToClipboard();
                RemoveSelected();
            }
        }

        public void PasteFromClipboard()
        {
            Console.WriteLine("Paste called");

            if (Clipboard.ContainsText())
            {
                XmlSerializer deserializer = new XmlSerializer(typeof(ClipboardWrapper));

                StringReader textReader = new StringReader(Clipboard.GetText());
                ClipboardWrapper data;
                try
                {
                    data = (ClipboardWrapper)deserializer.Deserialize(textReader);

                    foreach (Diagram diagram in data.Diagrams)
                    {
                        Nodes.Add(diagram);
                    }

                    foreach (Edge edge in data.Edges)
                    {
                        bool setA = false;
                        bool setB = false;

                        foreach (Diagram diagram in Nodes)
                        {
                            if (diagram.ID == edge.EndA.ID)
                            {
                                edge.EndA = diagram;
                                setA = true;
                            }
                            if (diagram.ID == edge.EndB.ID)
                            {
                                edge.EndB = diagram;
                                setB = true;
                            }
                        };

                        if (setA && setB)
                        {
                            Edges.Add(edge);
                        }
                    }

                    foreach (Diagram diagram in Nodes.Skip(Math.Max(0, Nodes.Count() - data.Diagrams.Count)).Take(data.Diagrams.Count))
                    {
                        diagram.ID = diagram.GetHashCode();
                        diagram.Opacity = 1;
                        diagram.Colour = "Black";
                    }
                }
                catch (Exception)
                {
                }
            }
        }

        public void ClearDocument()
        {
            Nodes.Clear();
            Edges.Clear();

            TranslateX = TranslateY = 0;
        }

        private void initTypes()
        {
            DiagramTypes.Add(typeof(ActivityDiagram), (int)EnumDiagramType.ActivityDiagram);
            DiagramTypes.Add(typeof(ClassDiagram), (int)EnumDiagramType.ClassDiagram);
            DiagramTypes.Add(typeof(InterfaceDiagram), (int)EnumDiagramType.InterfaceDiagram);

            //TODO: What exactly do we need the following lines for?
            EdgeTypes.Add(typeof(FullLineEdge), (int)EnumEdgeType.FullLineEdge);
            EdgeTypes.Add(typeof(DiamondEdge), (int)EnumEdgeType.DiamondEdge);
            EdgeTypes.Add(typeof(FilledDiamondEdge), (int)EnumEdgeType.FilledDiamondEdge);
            EdgeTypes.Add(typeof(ArrowEdge), (int)EnumEdgeType.ArrowEdge);
            EdgeTypes.Add(typeof(DashedArrowEdge), (int)EnumEdgeType.DashedArrowEdge);
        }
    }

    public enum EnumDiagramType : int
    {
        ActivityDiagram,
        ClassDiagram,
        InterfaceDiagram
    }

    public enum EnumEdgeType : int
    {
        FullLineEdge,
        DiamondEdge,
        FilledDiamondEdge,
        ArrowEdge,
        DashedArrowEdge

    }

    
}

